信号量是操作系统中重要的一部分,信号量一般用来进行资源管理和任务同步, FreeRTOS中信号量又分为二值信号量、 计数型信号量、互斥信号量和递归互斥信号量。

信号量简介

信号量在共享资源访问中的使用,信号量的另一个重要的应用场合就是任务同步,用于任务与任务或中断与任务之间的同步。 在执行中断服务函数的时候可以通过向任务发送信号量来通知任务它所期待的事件发生了。当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行。在编写中断服务函数的时候我们都知道一定要快进快出,中断服务函数里面不能放太多的代码,否则的话会影响的中断的实时性。

裸机编写中断服务函数的时候一般都只是在中断服务函数中打个标记,然后在其他的地方根据标记来做具体的处理过程。在
用 RTOS 系统的时候我们就可以借助信号量完成此功能, 当中断发生的时候就释放信号量,中断服务函数不做具体的处理。具体的处理过程做成一个任务,这个任务会获取信号量,如果获取到信号量就说明中断发生了,那么就开始完成相应的处理,这样做的好处就是中断执行时间非常短。

二值信号量

二值信号量的使命就是同步,完成任务与任务或中断与任务之间的同步。大多数情况下都是中断与任务之间的同步。

二值信号量简介

二值信号量其实就是一个只有一个队列项的队列,这个特殊的队列要么是满的,要么是空的,这不正好就是二值的吗? 任务和中断使用这个特殊队列不用在乎队列中存的是什么消息,只需要知道这个队列是满的还是空的。可以利用这个机制来完成任务与中断之间的同步。

二值信号量通常用于互斥访问或同步, 二值信号量和互斥信号量非常类似,但是还是有一些细微的差别, 互斥信号量拥有优先级继承机制, 二值信号量没有优先级继承。 因此二值信号另更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适合用于简单的互斥访问。

和队列一样,信号量 API 函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一一个信号量上的话那么优先级最高的哪个任务优先获得信号量, 这样当信号量有效的时候高优先级的任务就会解除阻塞状态。

在实际应用中通常会使用一个任务来处理 MCU 的某个外设,比如网络应用中,一般最简单的方法就是使用一个任务去轮询的查询 MCU 的 ETH(网络相关外设,如 STM32 的以太网MAC)外设是否有数据,当有数据的时候就处理这个网络数据。这样使用轮询的方式是很浪费CPU 资源的,而且也阻止了其他任务的运行。最理想的方法就是当没有网络数据的时候网络任务就进入阻塞态,把 CPU 让给其他的任务,当有数据的时候网络任务才去执行。

现在使用二值信号量就可以实现这样的功能,任务通过获取信号量来判断是否有网络数据,没有的话就进入阻塞态,而网络中断服务函数(大多数的网络外设都有中断功能,比如 STM32 的 MAC 专用 DMA中断,通过中断可以判断是否接收到数据)通过释放信号量来通知任务以太网外设接收到了网络数据,网络任务可以去提取处理了。 网络任务只是在一直的获取二值信号量,它不会释放信号量,而中断服务函数是一直在释放信号量,它不会获取信号量。在中断服务函数中发送信号量可以使用函数 xSemaphoreGiveFromISR(), 也可以使用任务通知功能来替代二值信号量,而且使用任务通知的话速度更快,代码量更少,

使用二值信号量来完成中断与任务同步的这个机制中,任务优先级确保了外设能够得到及
时的处理,这样做相当于推迟了中断处理过程。 也可以使用队列来替代二值信号量,在外设事
件的中断服务函数中获取相关数据,并将相关的数据通过队列发送给任务。如果队列无效的话
任务就进入阻塞态,直至队列中有数据,任务接收到数据以后就开始相关的处理过程。

创建二值信号量

同队列一样,要想使用二值信号量就必须先创建二值信号量

函数 描述
vSemaphoreCreateBinary () 动态创建二值信号量 ,这个是老版本FreeRTOS 中使用的创建二值信号量的 API 函

数。|
|xSemaphoreCreateBinary()|动态创建二值信号量,新版 FreeRTOS 使用此函数创建二值信号量。xSemaphoreCreateBinaryStatic() 静态创建二值信号量。|

函数 vSemaphoreCreateBinary ()

此函数是老版本 FreeRTOS 中的创建二值信号量函数,新版本已经不再使用了,此函数是个宏 ,具体创建过程是由函数xQueueGenericCreate()来完成的, 在文件 semphr.h 中有如下定义:

1
void vSemaphoreCreateBinary( SemaphoreHandle_t  xSemaphore )

参数:
xSemaphore:保存创建成功的二值信号量句柄。

返回值:
NULL: 二值信号量创建失败。
其他值: 二值信号量创建成功。

函数 xSemaphoreCreateBinary()

此函数是 vSemaphoreCreateBinary()的新版本,新版本的 FreeRTOS 中统一用此函数来创建二值信号量。 使用此函数创建二值信号量的话信号量所需要的 RAM 是由 FreeRTOS 的内存管理部分来动态分配的。此函数创建好的二值信号量默认是空的,也就是说刚创建好的二值信号量使用函数 xSemaphoreTake()是获取不到的,此函数也是个宏, 具体创建过程是由函数xQueueGenericCreate()来完成的, 函数原型如下:

1
SemaphoreHandle_t xSemaphoreCreateBinary( void )

参数:
xSemaphore:保存创建成功的二值信号量句柄。

返回值:
NULL: 二值信号量创建失败。
其他值: 二值信号量创建成功。

函数 xSemaphoreCreateBinaryStatic()

此函数也是创建二值信号量的,只不过使用此函数创建二值信号量的话信号量所需要的RAM需要由用户来分配,此函数是个宏,具体创建过程是通过函数 xQueueGenericCreateStatic()来完成的,函数原型如下:

1
SemaphoreHandle_t xSemaphoreCreateBinaryStatic( StaticSemaphore_t *pxSemaphoreBuffer )

参数:
pxSemaphoreBuffer: 此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。

返回值:
NULL: 二值信号量创建失败。
其他值: 创建成功的二值信号量句柄。

二值信号量创建过程分析

老版本的二值信号量动态创建函数 vSemaphoreCreateBinary(),函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )

#define vSemaphoreCreateBinary( xSemaphore )
{
( xSemaphore ) = xQueueGenericCreate( ( UBaseType_t ) 1, \ (1)
semSEMAPHORE_QUEUE_ITEM_LENGTH, \
queueQUEUE_TYPE_BINARY_SEMAPHORE );

if( ( xSemaphore ) != NULL )
{
( void ) xSemaphoreGive( ( xSemaphore ) ); \ (2)
}
}

#endif

(1)、上面说了二值信号量是在队列的基础上实现的,所以创建二值信号量就是创建队列的过程。这里使用函数 xQueueGenericCreate()创建了一个队列,队列长度为 1,队列项长度为 0,队列类型为 queueQUEUE_TYPE_BINARY_SEMAPHORE,也就是二值信号量。

(2)、当二值信号量创建成功以后立即调用函数 xSemaphoreGive()释放二值信号量,此时新创建的二值信号量有效。

新版本的二值信号量创建函数 xSemaphoreCreateBinary(),函数代码如下:

1
2
3
4
5
6
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
#define xSemaphoreCreateBinary() \
xQueueGenericCreate( ( UBaseType_t ) 1, \
semSEMAPHORE_QUEUE_ITEM_LENGTH, \
queueQUEUE_TYPE_BINARY_SEMAPHORE ) \
#endif

可以看出新版本的二值信号量创建函数也是使用函数 xQueueGenericCreate()来创建一个类型为 queueQUEUE_TYPE_BINARY_SEMAPHORE、长度为 1、队列项长度为 0 的队列。这一步和老版本的二值信号量创建函数一样,唯一不同的就是新版本的函数在成功创建二值信号量以后不会立即调用函数 xSemaphoreGive()释放二值信号量。 也就是说新版函数创建的二值信号量默认是无效的,而老版本是有效的。

释放信号量

函数 描述
xSemaphoreGive() 任务级信号量释放函数
xSemaphoreGiveFromISR() 中断级信号量释放函数

同队列一样,释放信号量也分为任务级和中断级,不管是二值信号量、计数型信号量还是互斥信号量,
递归互斥信号量有专用的释放函数。

函数 xSemaphoreGive()此函数用于释放二值信号量、计数型信号量或互斥信号量, 此函数是一个宏,真正释放信
号量的过程是由函数 xQueueGenericSend()来完成的, 函数原型如下:

1
BaseType_t xSemaphoreGive( xSemaphore )

参数:
xSemaphore: 要释放的信号量句柄。
返回值:
pdPASS: 释放信号量成功。
errQUEUE_FULL: 释放信号量失败。

xSemaphoreGive()此函数在文件 semphr.h 中有如下定义:

1
2
3
4
5
6
#define xSemaphoreGive( xSemaphore ) \

xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \
NULL, \
semGIVE_BLOCK_TIME, \
queueSEND_TO_BACK ) \

可以看出任务级释放信号量就是向队列发送消息的过程,只是这里并没有发送具体的消息,
阻塞时间为 0(宏 semGIVE_BLOCK_TIME 为 0),入队方式采用的后向入队,入队的时候队列结构体成员变量 uxMessagesWaiting 会加一,对于二,值信号量通过判断 uxMessagesWaiting 就可以知道信号量是否有效了,当 uxMessagesWaiting 为1 的话说明二值信号量有效,为 0 就无效。 如果队列满的话就返回错误值 errQUEUE_FULL,提示队列满,入队失败。

函数 xSemaphoreGiveFromISR()
此函数用于在中断中释放信号量, 此函数只能用来释放二值信号量和计数型信号量,绝对不能用来在中断服务函数中释放互斥信号量!此函数是一个宏,真正执行的是函数xQueueGiveFromISR(), 此函数原型如下:

1
2
BaseType_t xSemaphoreGiveFromISR( SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)

获取信号量

函数 描述
xSemaphoreTake() 任务级获取信号量函数
xSemaphoreTakeFromISR() 中断级获取信号量函数

函数 xSemaphoreTake()
此函数用于获取二值信号量、计数型信号量或互斥信号量,此函数是一个宏,真正获取信号量的过程是由函数 xQueueGenericReceive ()来完成的,函数原型如下:

1
2
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore,
TickType_t xBlockTime)

参数:
xSemaphore: 要获取的信号量句柄。
xBlockTime: 阻塞时间。

返回值:
pdTRUE: 获取信号量成功。
pdFALSE: 超时,获取信号量失败。

函数 xSemaphoreTakeFromISR ()
此函数用于在中断服务函数中获取信号量, 此函数用于获取二值信号量和计数型信号量,绝 对 不 能 使 用 此 函 数 来获取互斥信号量!此函数是一个宏 , 真正执行的是函数xQueueReceiveFromISR (),此函数原型如下:

1
2
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t * pxHigherPriorityTaskWoken)

计数型信号量

计数型信号量简介

不同与二值信号量,计数型信号量值可以大 1,这个最大值在创建信号量的时候可以设置。当计数型信号量有效的时候任务可以获取计数型信号量,信号量值只要大于 0 就表示计数型信号量有效。

有些资料中也将计数型信号量叫做数值信号量, 二值信号量相当于长度为 1 的队列,那么计数型信号量就是长度大于 1 的队列。 同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列是否为空即可。 计数型信号量通常用于如下两个场合:

事件计数

在这个场合中, 每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量 (信号量计数值减一,信号量值就是队列结构体成员变量uxMessagesWaiting)来处理事件。在这种场合中创建的计数型信号量初始计数值为 0。

资源管理

在这个场合中,信号量值代表当前资源的可用数量,比如停车场当前剩余的停车位数量。一个任务要想获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为 0 的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量,比如停车场一共有 100 个停车位,那么创建信号量的时候信号量值就应该初始化为 100。

创建计数型信号量

函数 描述
xSemaphoreCreateCounting() 使用动态方法创建计数型信号量。
xSemaphoreCreateCountingStatic() 使用静态方法创建计数型信号量

释放和获取计数信号量

计数型信号量的释放和获取与二值信号量相同

优先级翻转

在使用二值信号量的时候会遇到很常见的一个问题——优先级翻转,优先级翻转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果。

互斥信号量

斥信号量简介

互斥信号量其实就是一个拥有优先级继承的二值信号量, 在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。 互斥信号量适合用于那些需要互斥访问的应用中。 在互斥访问中互斥信号量相当于一个钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。

互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间,不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级, 这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的“优先级翻转”的影响降到最低。

优先级继承并不能完全的消除优先级翻转, 它只是尽可能的降低优先级翻转带来的影响。硬实时应用应该在设计之初就要避免优先级翻转的发生。互斥信号量不能用于中断服务函数中,原因如下:

● 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
● 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

创建互斥信号量

函数 描述
xSemaphoreCreateMutex() 使用动态方法创建互斥信号量。
xSemaphoreCreateMutexStatic() 使用静态方法创建互斥信号量。